package cn.cycleif.app.service;

import cn.cycleif.app.domain.dto.VoteDTO;
import cn.cycleif.app.mapper.AnswerMapper;
import cn.cycleif.app.mapper.QuestionMapper;
import cn.cycleif.app.mapper.VoteMapper;
import cn.cycleif.component.redis.RedisCache;
import cn.cycleif.constants.RedisConstant;
import cn.cycleif.entity.Question;
import cn.cycleif.entity.Vote;
import cn.cycleif.enums.ObjectTypeEnum;
import cn.cycleif.enums.VoteStatusEnum;
import cn.cycleif.util.IPUtils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.batch.MybatisBatch;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static cn.cycleif.constants.RedisConstant.*;

/**
 * @Date: 25/2/13-13:21
 * @Desc:
 */
@Service
@Slf4j
public class RedisService {

  @Autowired
  private RedisCache redisCache;

  @Autowired
  private QuestionMapper    questionMapper;
  @Autowired
  private AnswerMapper      answerMapper;
  @Autowired
  private SqlSessionFactory sqlSessionFactory;
  @Autowired
  private VoteMapper        voteMapper;

  public void addQuestionViewCount(Integer id) {
    String value = IPUtils.getIpAddr();
    String key   = RedisConstant.VIEW_COUNT + id;
    redisCache.hyperLogAdd(key, value);
  }

  public void syncQuestionViewCount() {
    LambdaQueryWrapper<Question> wrapper = new LambdaQueryWrapper<>();
    wrapper.select(Question::getId, Question::getViewCount);
    List<Question> questions = questionMapper.selectList(wrapper);
    questions.forEach(question -> {
      String  key   = RedisConstant.VIEW_COUNT + question.getId();
      Integer views = redisCache.getHyperLog(key);
      if (views > 0) question.setViewCount(views + question.getViewCount());
      questionMapper.updateById(question);
    });
  }

  /**
   * 获取redis问题阅读量，如果没有缓存，则返回 null
   * 需要加上数据库中数量
   *
   * @param id id
   * @return null | integer
   */
  public Integer getQuestionViewCount(Integer id) {
    String key = RedisConstant.VIEW_COUNT + id;
    if (Boolean.TRUE.equals(redisCache.hasKey(key))) return redisCache.getHyperLog(key);
    else return null;
  }

  public void agreeVote(VoteDTO dto) {
    voteByType(dto, VOTE_LIKE, VOTE_BAD, VoteStatusEnum.DISAGREE, VoteStatusEnum.AGREE);
  }

  public void disagreeVote(VoteDTO dto) {
    voteByType(dto, VOTE_BAD, VOTE_LIKE, VoteStatusEnum.AGREE, VoteStatusEnum.DISAGREE);
  }

  /**
   * 处理点赞，取消点赞，反对，取消反对等情况
   *
   * @param lastStatus   上次的状态
   * @param targetStatus 目标状态
   */
  private void voteByType(VoteDTO dto, String rKey1, String rKey2, VoteStatusEnum lastStatus, VoteStatusEnum targetStatus) {
    String key    = rKey1 + dto.getObjectType().getSign();
    String hKey   = key + ":" + dto.getObjectId();
    String userId = String.valueOf(dto.getUserId());
    String objId  = String.valueOf(dto.getObjectId());
    Vote   vote   = BeanUtil.copyProperties(dto, Vote.class);
    vote.setCreateAt(LocalDateTime.now());
    if (dto.getLastStatus() == lastStatus) { // 判断redis里是否有矛盾记录
      String bKey  = rKey2 + dto.getObjectType().getSign();
      String bHKey = bKey + ":" + dto.getObjectId();
      if (Boolean.TRUE.equals(redisCache.hExists(bHKey, userId))) { // 删除此矛盾记录
        redisCache.hDel(bHKey, userId);
        if (redisCache.hSize(hKey) == 0) redisCache.sRem(bKey, objId);
      } else { // 否则数据库里有记录，删除
        boolean result = deleteVoteByStatus(dto, userId, lastStatus);
        if (!result) return; // 乱传参或脏数据
      }
    }
    if (vote.getStatus() == targetStatus) { // 赞
      if (Boolean.FALSE.equals(redisCache.sIsMember(key, objId))) // 如果点赞set中没有记录
        redisCache.sAdd(key, objId); // 添加记录
      redisCache.hSet(hKey, userId, vote); // 记录点赞信息
    } else if (vote.getStatus() == VoteStatusEnum.NOTHING) { // 取消赞
      if (Boolean.TRUE.equals(redisCache.hExists(hKey, userId))) { // 如果缓存中有则删除
        redisCache.hDel(hKey, userId);
        if (redisCache.hSize(hKey) == 0) redisCache.sRem(key, objId);
      } else { // redis中无记录，删除数据库记录
        deleteVoteByStatus(dto, userId, targetStatus);
      }
    }
  }

  private boolean deleteVoteByStatus(VoteDTO dto, String userId, VoteStatusEnum statusEnum) {
    LambdaQueryWrapper<Vote> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(Vote::getUserId, userId).eq(Vote::getObjectId, dto.getObjectId())
           .eq(Vote::getObjectType, dto.getObjectType()).eq(Vote::getStatus, statusEnum);
    Vote dbVote = voteMapper.selectOne(wrapper);
    if (dbVote != null) voteMapper.deleteById(dbVote.getId());
    else {
      log.error("数据库中无此投票记录，脏数据或用户乱传参，用户id:{}，参数:{}", userId, dto);
      return false;
    }
    return true;
  }


  /**
   * 仅获取缓存中的数量，需要加上数据库中的数量
   *
   * @return null | integer
   */
  public Integer getVoteCnt(ObjectTypeEnum typeEnum, Integer id) {
    // 赞成减去反对
    String key  = VOTE_LIKE + typeEnum.getSign() + ":" + id;
    String bKey = VOTE_BAD + typeEnum.getSign() + ":" + id;
    if (Boolean.FALSE.equals(redisCache.hasKey(key)) && Boolean.FALSE.equals(redisCache.hasKey(bKey))) return null;
    long size = redisCache.hSize(key) - redisCache.hSize(bKey);
    return (int) size;
  }

  /**
   * 点赞落库
   */
  @Transactional
  public void syncVote() {
    String      questionKey  = VOTE_LIKE + ObjectTypeEnum.QUESTION.getSign();
    String      answerKey    = VOTE_LIKE + ObjectTypeEnum.ANSWER.getSign();
    Set<String> questIds     = redisCache.sMember(questionKey);
    Set<String> answerIds    = redisCache.sMember(answerKey);
    String      questionKey1 = VOTE_BAD + ObjectTypeEnum.QUESTION.getSign();
    String      answerKey1   = VOTE_BAD + ObjectTypeEnum.ANSWER.getSign();
    Set<String> questIds1    = redisCache.sMember(questionKey1);
    Set<String> answerIds1   = redisCache.sMember(answerKey1);


    List<Vote> votes  = syncDB(questionKey, questIds, questionMapper, true);
    List<Vote> votes1 = syncDB(answerKey, answerIds, answerMapper, true);
    List<Vote> votes3 = syncDB(questionKey1, questIds1, questionMapper, false);
    List<Vote> votes2 = syncDB(answerKey1, answerIds1, answerMapper, false);

    votes.addAll(votes1);
    votes.addAll(votes2);
    votes.addAll(votes3);
    // 删除redis中的key
    redisCache.delete(questionKey);
    redisCache.delete(answerKey);
    redisCache.delete(questionKey1);
    redisCache.delete(answerKey1);
    MybatisBatch<Vote>        mybatisBatch = new MybatisBatch<>(sqlSessionFactory, votes);
    MybatisBatch.Method<Vote> method       = new MybatisBatch.Method<>(VoteMapper.class);
    mybatisBatch.execute(method.insert());
  }

  /**
   * 同步表数据，包括vote、question等表
   *
   * @param key       redis key
   * @param objectIds 对象id
   * @param mapper    对象mapper
   * @param isAdd     是否是增加
   * @param <T>       mapper泛型
   * @return 产生的投票记录
   */
  private <T> List<Vote> syncDB(String key, Set<String> objectIds, BaseMapper<T> mapper, boolean isAdd) {
    List<Vote> votes = new ArrayList<>();
    for (String objectId : objectIds) {
      if (StringUtils.isEmpty(objectId)) continue;

      String       hKey = key + ":" + objectId;
      Long         size = redisCache.hSize(hKey);
      List<Object> list = redisCache.hVals(hKey);
      votes = list.stream().map(v -> JSONUtil.toBean(String.valueOf(v), Vote.class)).collect(Collectors.toList());

      UpdateWrapper<T> wrapper = new UpdateWrapper<>();
      wrapper.eq("id", objectId);
      if (isAdd) wrapper.setSql("vote_count=vote_count+" + size);
      else wrapper.setSql("vote_count=vote_count-" + size);
      mapper.update(wrapper);

      redisCache.delete(hKey);
    }
    return votes;
  }


  public VoteStatusEnum getVoteStatus(ObjectTypeEnum typeEnum, Integer id, Integer userId) {
    String sUserId = String.valueOf(userId);
    // 是否有点赞缓存
    String         key = VOTE_LIKE + typeEnum.getSign() + ":" + id;
    VoteStatusEnum status;
    status = getVoteStatusFromCache(key, sUserId);
    if (status != null) return status;
    // 是否有点踩缓存
    String bKey = VOTE_BAD + typeEnum.getSign() + ":" + id;
    status = getVoteStatusFromCache(bKey, sUserId);
    if (status != null) return status;
    // 是否有空对象缓存
    Boolean member = redisCache.sIsMember(VOTE_NULL + typeEnum.getSign() + ":" + id, sUserId);
    if (Boolean.TRUE.equals(member)) return VoteStatusEnum.NOTHING;
    // 是否有其他缓存
    String cacheKey = VOTE_COUNT + "cache:" + typeEnum.getSign();
    status = getVoteStatusFromCache(cacheKey + ":" + id, sUserId);
    if (status != null) return status;
    // db查询
    LambdaQueryWrapper<Vote> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(Vote::getUserId, userId).eq(Vote::getObjectId, id).eq(Vote::getObjectType, typeEnum);
    Vote dbVote = voteMapper.selectOne(wrapper);
    if (dbVote == null) { // 缓存，db无
      // redis加空对象
      redisCache.sAdd(VOTE_NULL + typeEnum.getSign() + ":" + id, sUserId);
      redisCache.expire(VOTE_NULL + typeEnum.getSign() + ":" + id, VOTE_NULL_EXPIRATION, TimeUnit.MINUTES);
      return VoteStatusEnum.NOTHING;
    } else { // 缓存无，db有
      redisCache.hSet(cacheKey + ":" + id, sUserId, dbVote);
      redisCache.expire(cacheKey, VOTE_NULL_EXPIRATION, TimeUnit.MINUTES);
      return dbVote.getStatus();
    }
  }

  private VoteStatusEnum getVoteStatusFromCache(String key, String hKey) {
    if (Boolean.TRUE.equals(redisCache.hExists(key, hKey))) {
      Object o    = redisCache.hGet(key, hKey);
      Vote   vote = JSONUtil.toBean(String.valueOf(o), Vote.class);
      return vote.getStatus();
    }
    return null;
  }
}
